延續 Day 3 的 Eq/Field/And
,本篇把日常最常見、也是最容易出錯的進階運算子一次講清楚:$gt
、$gte
、$lt
、$lte
、$in
、$nin
、$elemMatch
,以及「Array 欄位的自動包裝」行為
require 'mongory'
Mongory.enable_symbol_snippets!
Mongory.register(Array)
records = [
{ 'name' => 'Jack', 'age' => 18, 'status' => 'active', 'tags' => [%w(ruby lv6), %w(rails lv3)] },
{ 'name' => 'Jill', 'age' => 15, 'status' => 'pending', 'tags' => [%w(go lv5)] },
{ 'name' => 'Bob', 'age' => 21, 'status' => 'inactive', 'tags' => [%w(ruby lv8)] }
]
# 1) $gt / $gte / $lt / $lte:數值比較
puts "-- gt"; p records.mongory.where(:age.gt => 18).pluck("name")
# ["Bob"]
puts "-- gte"; p records.mongory.where(:age.gte => 18).pluck("name")
# ["Jack", "Bob"]
puts "-- lt"; p records.mongory.where(:age.lt => 18).pluck("name")
# ["Jill"]
puts "-- lte"; p records.mongory.where(:age.lte => 18).pluck("name")
# ["Jack", "Jill"]
# 2) $in / $nin:集合包含(注意:條件必須是 Array)
puts "-- in"; p records.mongory.where(:status.in => %w(active pending)).pluck("name")
# ["Jack", "Jill"]
puts "-- nin"; p records.mongory.where(:status.nin => %w(active pending)).pluck("name")
# ["Bob"]
# 3) $elemMatch:陣列元素匹配(巢狀條件)
puts "-- elemMatch";
p records.mongory
.where(:tags.elem_match => { 0 => 'ruby' }) # 以簡單示例表示第一個欄位含 'ruby'
.pluck("name")
# ["Jack", "Bob"]
筆者建議搭配 explain
驗證語意是否如預期:
q = records.mongory.where(:age.gte => 18, :status.in => %w[active pending])
q.explain
輸出:
$elemMatch
MongoDB 語意下,若欄位本身是 Array (or embeds many),且條件是「單值」,會自動用 $elemMatch
轉為「元素存在即可」
Mongory 也遵循同樣邏輯
records = [
{ 'tags' => %w[ruby lv6] },
{ 'tags' => %w[go lv5] }
]
records.mongory.where(:tags => 'ruby').each { |r| p r } # 等價於 :tags.elem_match => { :$eq => 'ruby' }
explain 會顯示陣列欄位被包裝為 ElemMatch
的樹:
Field: "tags" to match: {"$elemMatch"=>"ruby"}
└─ ElemMatch: "ruby"
└─ Eq: "ruby"
# ElemMatch + Eq 其實就是 array.include?(x) 的意思
$in/$nin
對「Array 欄位」不是 includes?,而是「交集」若欄位是 Array,$in
會做「交集是否非空」,$nin
會做「交集是否為空」
records = [ { 'tags' => %w[a e] } ]
# $in: 任一元素命中即可
cond = { :tags.in => %w[a b c] }
p records.mongory.where(cond).to_a.any? # => true(a 命中)
# $nin: 所有元素都不在條件集合內才算通過
cond = { :tags.nin => %w[a b c] }
p records.mongory.where(cond).to_a.any? # => false(a 擋下)
$in/$nin
的條件必須是 Array(否則會觸發型別錯誤)
$elemMatch
的條件必須是 Hash;其內部可再接其它運算子(例如 :priority.gt => 5
)
條件式若遇到欄位是 Array 則會自動將條件包裝在 $elemMatch
下
空集合語意要留心:$in: []
通常不會匹配任何東西;$nin: []
等同不過濾
正規表達式與字串比較的差異:/J/
與 "J"
語意不同,Regex
能命中部分字串,Eq
則需全等
$gt/$gte/$lt/$lte
:數值比較,gte/lte
含邊界
$in/$nin
:集合比較;欄位為 Array 時採交集語意
$elemMatch
:針對 Array 之元素逐一匹配
單值條件遇到 Array 欄位時會自動用 $elemMatch
包裝條件
require 'mongory'
Mongory.enable_symbol_snippets!
Mongory.register(Array)
users = [
{ 'name' => 'Ann', 'age' => 19, 'status' => 'active', 'tags' => [{ 'name' => 'ruby', 'priority' => 6 }] },
{ 'name' => 'Ben', 'age' => 22, 'status' => 'pending', 'tags' => [{ 'name' => 'rails', 'priority' => 3 }] },
{ 'name' => 'Cody', 'age' => 17, 'status' => 'active', 'tags' => [{ 'name' => 'ruby', 'priority' => 8 }] }
]
q = users.mongory
.where(:age.gte => 18)
.where(:status.in => %w[active pending])
.where(:tags.elem_match => { :name => 'ruby', :priority.gt => 5 })
q.explain
q.each { |u| p u['name'] } # 預期:只會有 Ann(Cody age < 18 不會命中; Ben tags name 是 rails 也不通過)
筆者在專案實戰裡,會固定以 explain
確認運算子樹形結構是否與心智模型一致,再以小型資料集跑 trace
驗證逐步匹配過程;這能有效降低條件錯配與維護成本
接下來 Day 5 會詳細說明「單子條件解包與層級消除」是什麼